﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Mono.Cecil;
using Mono.Cecil.Cil;
using System.Reflection;
using System.Threading;
using System.IO;

namespace zPod.zInject
{
    public class Injector
    {
        #region Public Constructors
        public Injector()
        {
			_injectedMethods = new List<string>();
        }
        #endregion

		public List<string> _injectedMethods;
		public List<string> InjectedMethods
		{
			get
			{
				return new List<string>(_injectedMethods);
			}
		}

        #region Public Methods
        public AssemblyDefinition Inject(string assemblyPath)
        {
			AppDomain newDomain = AppDomain.CreateDomain("InjectionDomain");
			//var originalAssembly = Assembly.LoadFile(Path.GetFullPath(assemblyPath));
			//var exportedTypes = originalAssembly.GetExportedTypes();

			var assembly = AssemblyFactory.GetAssembly(assemblyPath);
			

            foreach (ModuleDefinition module in assembly.Modules)
            {
                foreach (TypeDefinition type in module.Types)
                {
                    foreach (MethodDefinition method in type.Methods)
                    {
						var first = method.CustomAttributes.Cast<Mono.Cecil.CustomAttribute>().FirstOrDefault();

                        var attributeTypes =
                            from customAttr in method.CustomAttributes.Cast<Mono.Cecil.CustomAttribute>()
							select GetCustomAttributeType(customAttr);

                        // Solo inyectar si tiene atributos para inyectar y no fue inyectado aún
                        if (attributeTypes.All(attributeType => attributeType != typeof(InjectedAttribute)) &&
                            attributeTypes.Any(attributeType => attributeType.IsSubclassOf(typeof(InjectionAttributeBase))))
                        {
							_injectedMethods.Add(String.Format("{0}.{1}",
								method.DeclaringType.FullName,
								method.Name));
							//Lo marco como ya inyectado
                            method.CustomAttributes.Add(
                                new CustomAttribute(
                                    assembly.MainModule.Import(typeof(InjectedAttribute)
                                        .GetConstructor(new Type[] { }))));


                            var worker = method.Body.CilWorker;

                            //Llamo al ExecuteBefore
                            var firstInstruction = method.Body.Instructions[0];
                            var before1 = worker.Create(OpCodes.Ldstr, method.Name);
                            var before2 = worker.Create(OpCodes.Ldarg_0);
                            var before3 = worker.Create(OpCodes.Call, assembly.MainModule.Import(typeof(Injector).GetMethod("ExecuteBefore")));

                            worker.InsertBefore(firstInstruction, before1);
                            worker.InsertBefore(firstInstruction, before2);
                            worker.InsertBefore(firstInstruction, before3);


                            //Llamo al ExecuteAfter
                            var lastInstruction = method.Body.Instructions[method.Body.Instructions.Count - 1];
                            var after1 = worker.Create(OpCodes.Ldstr, method.Name);
                            var after2 = worker.Create(OpCodes.Ldarg_0);
                            var after3 = worker.Create(OpCodes.Call, assembly.MainModule.Import(typeof(Injector).GetMethod("ExecuteAfter")));

                            worker.InsertBefore(lastInstruction, after1);
                            worker.InsertBefore(lastInstruction, after2);
                            worker.InsertBefore(lastInstruction, after3);

                        }
                    }
                }
            }

            return assembly;
        }

		private static Type GetCustomAttributeType(CustomAttribute customAttr)
		{
			string assemblyName = String.Empty;
			var scope = customAttr.Constructor.DeclaringType.Scope;
			var moduleDefinition = scope as ModuleDefinition;
			var assemblyNameReference = scope as AssemblyNameReference;
			if (moduleDefinition != null)
			{
				assemblyName = moduleDefinition.Assembly.Name.FullName;
			}
			else if (assemblyNameReference != null)
			{
				assemblyName = assemblyNameReference.FullName;
			}
			else
			{
				throw new Exception("Unable to handle scope type: " + scope.GetType().FullName);
			}
			
			return Type.GetType(String.Format("{0}, {1}",
				customAttr.Constructor.DeclaringType.FullName,
				assemblyName, true));
		}

        private static object CreateCustomAttributeInstance(CustomAttribute customAttr, List<Object> attrParams)
        {
            var customAttributeType = Type.GetType(customAttr.Constructor.DeclaringType.FullName);
            var customAttributeObject = Activator.CreateInstance(
                customAttributeType,
                attrParams.ToArray()
                );
            return customAttributeObject;
        }

        private static List<Object> GetCustomAttributeParameters(CustomAttribute customAttr)
        {
            List<Object> attrParams = new List<object>(customAttr.ConstructorParameters.Count);
            foreach (var param in customAttr.ConstructorParameters)
            {
                attrParams.Add(param);
            }
            return attrParams;
        }

        public static void ExecuteBefore(string methodName, object target)
        {
            var methodInfo = target.GetType().GetMethod(methodName);
            var customAttributes = methodInfo.GetCustomAttributes(true).Where(c => c is InjectionAttributeBase).Cast<InjectionAttributeBase>();

            foreach (var attr in customAttributes.OrderByDescending(ca => ca.Priority))
            {
                attr.BeforeExecute(methodName, target);
            }
        }

        public static void ExecuteAfter(string methodName, object target)
        {
            var methodInfo = target.GetType().GetMethod(methodName);
            var customAttributes = methodInfo.GetCustomAttributes(true).Where(c => c is InjectionAttributeBase).Cast<InjectionAttributeBase>();

            foreach (var attr in customAttributes.OrderBy(ca => ca.Priority))
            {
                attr.AfterExecute(methodName, target);
            }
        }
        #endregion
    }
}
